Coverage Report

Created: 2025-11-30 10:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
D:\a\csshw\csshw\src\utils\config.rs
Line
Count
Source
1
//! Client and Daemon configuration structs.
2
3
use serde_derive::{Deserialize, Serialize};
4
use std::env;
5
use windows::Win32::System::Console::{
6
    BACKGROUND_INTENSITY, BACKGROUND_RED, FOREGROUND_BLUE, FOREGROUND_GREEN, FOREGROUND_INTENSITY,
7
    FOREGROUND_RED,
8
};
9
10
/// Placeholder for the `<username>@<host>` argument to the chosen SSH program.
11
const DEFAULT_USERNAME_HOST_PLACEHOLDER: &str = "{{USERNAME_AT_HOST}}";
12
13
/// Representation of the project configuration.
14
///
15
/// Includes subcommand specific configurations for `client` and `daemon` subcommands
16
/// as well es the cluster tags.
17
#[derive(Serialize, Deserialize, Default, PartialEq, Debug)]
18
pub struct Config {
19
    /// List of cluster tags.
20
    ///
21
    /// Includes the name of the cluster tag and a list of hostnames.
22
    pub clusters: Vec<Cluster>,
23
    /// Configuration relevant for the `client` subcommand.
24
    pub client: ClientConfig,
25
    /// Configuration relevant for the `daemon` subcommand.
26
    pub daemon: DaemonConfig,
27
}
28
29
/// Representation of the project configuration
30
/// where everything is optional.
31
///
32
/// Used to handle cases where only some or none of the configurations are present.
33
/// Enables backwards compatiblity with configuration files written by older versions.
34
#[derive(Serialize, Deserialize, Default)]
35
pub struct ConfigOpt {
36
    #[allow(missing_docs)]
37
    pub clusters: Option<Vec<Cluster>>,
38
    #[allow(missing_docs)]
39
    pub client: Option<ClientConfigOpt>,
40
    #[allow(missing_docs)]
41
    pub daemon: Option<DaemonConfigOpt>,
42
}
43
44
impl From<ConfigOpt> for Config {
45
    /// Unwraps the existing configuration values or applies the default.
46
15
    fn from(val: ConfigOpt) -> Self {
47
15
        return Config {
48
15
            clusters: val.clusters.unwrap_or_default(),
49
15
            client: val.client.unwrap_or_default().into(),
50
15
            daemon: val.daemon.unwrap_or_default().into(),
51
15
        };
52
15
    }
53
}
54
55
impl From<Config> for ConfigOpt {
56
    /// Wraps all configuration values as options.
57
1
    fn from(val: Config) -> Self {
58
1
        return ConfigOpt {
59
1
            clusters: Some(val.clusters),
60
1
            client: Some(val.client.into()),
61
1
            daemon: Some(val.daemon.into()),
62
1
        };
63
1
    }
64
}
65
66
/// Representation of a cluster tag.
67
#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)]
68
pub struct Cluster {
69
    /// Name of the cluster tag, used to identify it.
70
    pub name: String,
71
    /// List of hostnames the cluster tag is an alias for.
72
    pub hosts: Vec<String>,
73
}
74
75
/// Representation of the `client` subcommand configurations.
76
#[derive(Serialize, Deserialize, PartialEq, Debug)]
77
pub struct ClientConfig {
78
    /// Full path to the SSH config.
79
    ///
80
    /// # Example
81
    ///
82
    /// `'C:\Users\<username>\.ssh\config'`
83
    pub ssh_config_path: String,
84
    /// Name of the program used to establish the SSH connection.
85
    /// # Example
86
    ///
87
    /// `'ssh'`
88
    pub program: String,
89
    /// List of arguments provided to the program.
90
    ///
91
    /// Must include the `username_host_placeholder`.
92
    ///
93
    /// # Example
94
    ///
95
    /// `['-XY', '{{USERNAME_AT_HOST}}']`
96
    pub arguments: Vec<String>,
97
    /// Placeholder string used to inject `<user>@<host>` into the list of arguments.
98
    ///
99
    /// # Example
100
    ///
101
    /// `'{{USERNAME_AT_HOST}}'`
102
    pub username_host_placeholder: String,
103
}
104
105
impl Default for ClientConfig {
106
    /// Returns a sensible default `ClientConfig`.
107
    ///
108
    /// # Returns
109
    ///
110
    /// `ClientConfig` with the following values:
111
    /// * `ssh_config_path`             - `%USERPROFILE%\.ssh\config`
112
    /// * `program`                     - `ssh`
113
    /// * `arguments`                   - `-XY {{USERNAME_AT_HOST}}`
114
    /// * `usernamt_host_placeholder`   - `{{USERNAME_AT_HOST}}`
115
    ///
116
    /// Note: %USERPROFILE% actually is resolved by us, so the actual value
117
    ///       is whatever the environment variable at runtime points to.
118
60
    fn default() -> Self {
119
60
        return ClientConfig {
120
60
            ssh_config_path: format!("{}\\.ssh\\config", env::var("USERPROFILE").unwrap()),
121
60
            program: "ssh".to_string(),
122
60
            arguments: vec![
123
60
                "-XY".to_string(),
124
60
                DEFAULT_USERNAME_HOST_PLACEHOLDER.to_string(),
125
60
            ],
126
60
            username_host_placeholder: DEFAULT_USERNAME_HOST_PLACEHOLDER.to_string(),
127
60
        };
128
60
    }
129
}
130
131
/// Representation of the `client` subcommand configurations
132
/// where everything is optional.
133
#[derive(Serialize, Deserialize)]
134
pub struct ClientConfigOpt {
135
    #[allow(missing_docs)]
136
    pub ssh_config_path: Option<String>,
137
    #[allow(missing_docs)]
138
    pub program: Option<String>,
139
    #[allow(missing_docs)]
140
    pub arguments: Option<Vec<String>>,
141
    #[allow(missing_docs)]
142
    pub username_host_placeholder: Option<String>,
143
}
144
145
impl Default for ClientConfigOpt {
146
13
    fn default() -> Self {
147
13
        return ClientConfig::default().into();
148
13
    }
149
}
150
151
impl From<ClientConfigOpt> for ClientConfig {
152
    /// Unwraps the existing configuration values or applies the default.
153
18
    fn from(val: ClientConfigOpt) -> Self {
154
18
        let default = ClientConfig::default();
155
18
        return ClientConfig {
156
18
            ssh_config_path: val.ssh_config_path.unwrap_or(default.ssh_config_path),
157
18
            program: val.program.unwrap_or(default.program),
158
18
            arguments: val.arguments.unwrap_or(default.arguments),
159
18
            username_host_placeholder: val
160
18
                .username_host_placeholder
161
18
                .unwrap_or(default.username_host_placeholder),
162
18
        };
163
18
    }
164
}
165
166
impl From<ClientConfig> for ClientConfigOpt {
167
    /// Wraps all configuration values as options.
168
14
    fn from(val: ClientConfig) -> Self {
169
14
        return ClientConfigOpt {
170
14
            ssh_config_path: Some(val.ssh_config_path),
171
14
            program: Some(val.program),
172
14
            arguments: Some(val.arguments),
173
14
            username_host_placeholder: Some(val.username_host_placeholder),
174
14
        };
175
14
    }
176
}
177
178
/// Representation of the `daemon` subcommand configurations.
179
#[derive(Serialize, Deserialize, PartialEq, Debug)]
180
pub struct DaemonConfig {
181
    /// Height in pixel of the daemon console window.
182
    ///
183
    /// Note: we are [DPI Unaware][1] which means the number of pixels
184
    ///       represents the `logical` scale, not the physical.
185
    ///
186
    /// [1]: https://learn.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows#dpi-unaware
187
    pub height: i32,
188
    /// Controls how the client console windows make use of the available screen space.
189
    ///
190
    /// * `> 0.0` - Aims for vertical rectangle shape.
191
    ///             The larger the value, the more exaggerated the "verticality".
192
    ///             Eventually the windows will all be columns.
193
    /// * `= 0.0` - Aims for square shape.
194
    /// * `< 0.0` - Aims for horizontal rectangle shape.
195
    ///             The smaller the value, the more exaggerated the "horizontality".
196
    ///             Eventually the windows will all be rows.
197
    ///             `-1.0` is the sweetspot for mostly preserving a 16:9 ratio.
198
    pub aspect_ratio_adjustement: f64,
199
    /// Controls back- and foreground colors of the daemon console window.
200
    ///
201
    /// All [standard windows color combinations][1] are available:
202
    ///
203
    /// FOREGROUND_BLUE:        1   \
204
    /// FOREGROUND_GREEN:       2   \
205
    /// FOREGROUND_RED:         4   \
206
    /// FOREGROUND_INTENSITY:   8   \
207
    /// BACKGROUND_BLUE:        16  \
208
    /// BACKGROUND_GREEN:       32  \
209
    /// BACKGROUND_RED:         64  \
210
    /// BACKGROUND_INTENSITY:   128 \
211
    ///
212
    /// # Example
213
    ///
214
    /// White font on red background: 8 + 4 + 2 + 1 + 128 + 64 = `207`
215
    ///
216
    /// [1]: https://learn.microsoft.com/en-us/windows/console/console-screen-buffers#character-attributes
217
    pub console_color: u16,
218
}
219
220
impl Default for DaemonConfig {
221
    /// Returns a sensible default `DaemonConfig`.
222
    ///
223
    /// # Returns
224
    ///
225
    /// `DaemonConfig` with the following values:
226
    /// * `height`                      - `200`
227
    /// * `aspect_ratio_adjustment`    - `-1.0`
228
    /// * `console_color`               - `207`
229
59
    fn default() -> Self {
230
59
        return DaemonConfig {
231
59
            height: 200,
232
59
            aspect_ratio_adjustement: -1f64,
233
59
            console_color: (FOREGROUND_INTENSITY
234
59
                | FOREGROUND_RED
235
59
                | FOREGROUND_GREEN
236
59
                | FOREGROUND_BLUE
237
59
                | BACKGROUND_INTENSITY
238
59
                | BACKGROUND_RED)
239
59
                .0,
240
59
        };
241
59
    }
242
}
243
244
/// Representation of the `daemon` subcommand configurations
245
/// where everything is optional.
246
#[derive(Serialize, Deserialize)]
247
pub struct DaemonConfigOpt {
248
    #[allow(missing_docs)]
249
    pub height: Option<i32>,
250
    #[allow(missing_docs)]
251
    pub aspect_ratio_adjustement: Option<f64>,
252
    #[allow(missing_docs)]
253
    pub console_color: Option<u16>,
254
}
255
256
impl Default for DaemonConfigOpt {
257
12
    fn default() -> Self {
258
12
        return DaemonConfig::default().into();
259
12
    }
260
}
261
262
impl From<DaemonConfigOpt> for DaemonConfig {
263
    /// Unwraps the existing configuration values or applies the default.
264
18
    fn from(val: DaemonConfigOpt) -> Self {
265
18
        let default = DaemonConfig::default();
266
18
        return DaemonConfig {
267
18
            height: val.height.unwrap_or(default.height),
268
18
            aspect_ratio_adjustement: val
269
18
                .aspect_ratio_adjustement
270
18
                .unwrap_or(default.aspect_ratio_adjustement),
271
18
            console_color: val.console_color.unwrap_or(default.console_color),
272
18
        };
273
18
    }
274
}
275
276
impl From<DaemonConfig> for DaemonConfigOpt {
277
    /// Wraps all configuration values as options.
278
13
    fn from(val: DaemonConfig) -> Self {
279
13
        return DaemonConfigOpt {
280
13
            height: Some(val.height),
281
13
            aspect_ratio_adjustement: Some(val.aspect_ratio_adjustement),
282
13
            console_color: Some(val.console_color),
283
13
        };
284
13
    }
285
}
286
287
#[cfg(test)]
288
#[path = "../tests/utils/test_config.rs"]
289
mod test_config;